package org.infinitest;

import static java.util.logging.Level.*;
import static org.infinitest.util.InfinitestUtils.*;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.bcel.classfile.JavaClass;
import org.infinitest.core.InfinitestBuilder;
import org.infinitest.core.InfinitestCore;
import org.infinitest.core.JUnit4Runner;
import org.infinitest.core.TestEvent;
import org.infinitest.swingui.ResultCollector;
import org.infinitest.util.InfinitestUtils;

import com.reeltwo.jumble.mutation.Mutater;

public class DependencyExperiment implements Iterable<String>
{
  private Set<String> projectClasses;
  private int inconsistencies;
  private int testRuns;
  private InfinitestCore core;
  private ResultCollector resultCollector;
  private long minimalTestTime;
  private long allTestTime;
  private static PrintStream out = System.out;

  public DependencyExperiment(String[] args)
  {
    resultCollector = new ResultCollector();
    classpath = createClaspath(args, System.getProperty("java.class.path"));
    InfinitestBuilder builder = new InfinitestBuilder(classpath);
    builder.setTestRunner(JUnit4Runner.class);
    builder.setResultCollector(resultCollector);
    core = builder.getCore();
  }

  public static void main(String[] args) throws Exception
  {
    out.println("Starting experiment...");
    new DependencyExperiment(args).run();
  }

  private void run() throws Exception
  {
    out.println("Indexed " + getClassCount() + " classes");
    runAllTests();
    verifyNoFailures();

    for (String clazz : this)
    {
      File backup = createBackup(clazz);
      String mutation = mutateClass(clazz);
      List<TestEvent> minimalResults = runMinimalTests();
      List<TestEvent> allResults = runAllTests();
      compareResults(allResults, minimalResults, mutation);
      restoreFromBackup(backup);
    }
    printStatistics();
    System.exit(0);
  }

  void verifyNoFailures()
  {
    if (resultCollector.hasFailures())
      throw new IllegalStateException("Unexpected failures in project + " + resultCollector.getFailures());
  }

  int getClassCount()
  {
    return getProjectClasses().size();
  }

  void compareResults(List<TestEvent> allResults, List<TestEvent> minimalResults, String mutation)
  {
    testRuns++;
    if (sameContents(allResults, minimalResults))
      out.print(".");
    else
    {
      inconsistencies++;
      out.println("F");
      InfinitestUtils.log(WARNING, "Results differ for change: " + mutation);
      InfinitestUtils.log(WARNING, "Infinitest results: " + minimalResults.toString());
      InfinitestUtils.log(WARNING, "AllTest results" + allResults.toString());
    }
    resultCollector.clear();
  }

  private boolean sameContents(List<TestEvent> allResults, List<TestEvent> minimalResults)
  {
    return new HashSet<TestEvent>(allResults).equals(new HashSet<TestEvent>(minimalResults));
  }

  List<TestEvent> runMinimalTests()
  {
    long timestamp = System.currentTimeMillis();
    core.update();
    minimalTestTime += System.currentTimeMillis() - timestamp;
    return resultCollector.getFailures();
  }

  void printStatistics()
  {
    out.println("Ran " + testRuns + " test runs");
    out.println("Found " + inconsistencies + " inconsistencies");
    out.println("AllTest runs took " + allTestTime + "ms");
    out.println("MinimalTest runs took " + minimalTestTime + "ms");
  }

  List<TestEvent> runAllTests()
  {
    core.reload();
    long timestamp = System.currentTimeMillis();
    core.update();
    allTestTime += System.currentTimeMillis() - timestamp;
    return resultCollector.getFailures();
  }

  String mutateClass(String className) throws ClassNotFoundException, IOException
  {
    Mutater mutater = createMutater();
    JavaClass mutatedClass = mutater.jumbler(className);
    mutatedClass.dump(getFileForClass(className));
    return mutater.getModification();
  }

  private static Mutater createMutater()
  {
    Mutater mutater = new Mutater();
    mutater.setMutateCPool(true);
    mutater.setMutateIncrements(true);
    mutater.setMutateInlineConstants(true);
    mutater.setMutateNegs(true);
    mutater.setMutateReturnValues(true);
    mutater.setMutateSwitch(true);
    return mutater;
  }

  public Iterator<String> iterator()
  {
    return getProjectClasses().iterator();
  }

  private Set<String> getProjectClasses()
  {
    if (projectClasses == null)
      projectClasses = new HashSet<String>(core.getAllIndexedClasses());
    return projectClasses;
  }

  public void setCore(InfinitestCore core)
  {
    this.core = core;
  }

  public int getInconsistencies()
  {
    return inconsistencies;
  }

  void addFailure(TestEvent testEvent)
  {
    resultCollector.testFailed(testEvent);
  }

  public void setOutputStream(PrintStream outputStream)
  {
    out = outputStream;
  }

  // DEBT
  // Duplicated in InfinitestTestUtils

  public File getFileForClass(Class<?> classToModify)
  {
    // DEBT
    // Assumes one class directory
    File file = InfinitestUtils.getFileForClass(classpath.getClassDirs().get(0), classToModify.getName());
    if (!file.exists())
      throw new IllegalArgumentException(file + " does not exist");
    return file;
  }

  // DEBT
  // Assumes one class directory
  public File getFileForClass(String className)
  {
    File file = InfinitestUtils.getFileForClass(classpath.getClassDirs().get(0), className);
    if (!file.exists())
      throw new IllegalArgumentException(file + " does not exist");
    return file;
  }

  public File createBackup(String className) throws Exception
  {
    File originalFile = getFileForClass(className);
    File backupFile = new File(originalFile.getAbsolutePath() + BACKUP_EXT);
    InfinitestUtils.copyFile(originalFile, backupFile);
    return backupFile;
  }

  public void restoreFromBackup(File backup)
  {
    File originalFile = new File(backup.getAbsolutePath().replace(BACKUP_EXT, ""));
    if (!backup.renameTo(originalFile))
      throw new IllegalStateException(originalFile + " could not be restored");
  }

  private static final String BACKUP_EXT = ".infinitest_bak";
  private Classpath classpath;
}
